<!DOCTYPE html>
<html lang="en">

<head>
    <title>three.js webgl - skinning and morphing</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <link type="text/css" rel="stylesheet" href="main.css">
    <style>
        body {
            color: #222;
        }

        a {
            color: #2fa1d6;
        }

        p {
            max-width: 600px;
            margin-left: auto;
            margin-right: auto;
            padding: 0 2em;
        }
    </style>
</head>

<body>
    <div id="info">
        <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - skinning and morphing<br />
        <p>
            The animation system allows clips to be played individually, looped, or crossfaded with other clips. This
            example shows a character looping in one of several base animation states, then transitioning smoothly to
            one-time actions. Facial expressions are controlled independently with morph targets.
        </p>
        Model by
        <a href="https://www.patreon.com/quaternius" target="_blank" rel="noopener">Tomás Laulhé</a>,
        modifications by <a href="https://donmccurdy.com/" target="_blank" rel="noopener">Don McCurdy</a>. CC0.<br />
    </div>

    <script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

    <script type="module">

        import * as THREE from 'three';

        import Stats from 'three/addons/libs/stats.module.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

        let container, stats, clock, gui, mixer, actions, activeAction, previousAction;
        let camera, scene, renderer, model, face;

        const api = { state: 'Walking' };

        init();
        animate();

        function init() {

            container = document.createElement('div');//内容
            document.body.appendChild(container);//内容添加到Body

            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 100);//初始化相机
            camera.position.set(- 5, 3, 10);//设置相机位置
            camera.lookAt(0, 2, 0);//设置相机方向

            scene = new THREE.Scene();//初始化场景
            scene.background = new THREE.Color(0xe0e0e0);//设置场景背景颜色
            scene.fog = new THREE.Fog(0xe0e0e0, 20, 100);//设置场景雾化

            clock = new THREE.Clock();//初始化时钟

            // ground

            // lights

            const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 3);//初始化灯光（半球光）
            hemiLight.position.set(0, 20, 0);//灯光位置
            scene.add(hemiLight);//场景添加灯光

            const dirLight = new THREE.DirectionalLight(0xffffff, 3);//初始化定向光
            dirLight.position.set(0, 20, 10);//定向光位置
            scene.add(dirLight);//添加定向光

            // ground

            const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000), new THREE.MeshPhongMaterial({ color: 0xcbcbcb, depthWrite: false }));//初始化网格（地面）
            mesh.rotation.x = - Math.PI / 2;//
            scene.add(mesh);

            const grid = new THREE.GridHelper(200, 40, 0x000000, 0x000000);//初始化网格（辅助网格）
            grid.material.opacity = 0.2;//
            grid.material.transparent = true;
            scene.add(grid);//添加网格

            // model

            const loader = new GLTFLoader();//初始化渲染器
            loader.load('models/gltf/RobotExpressive/RobotExpressive.glb', function (gltf) {
                model = gltf.scene;//初始化模型 3dObject
                scene.add(model);
                createGUI(model, gltf.animations);
            }, undefined, function (e) {

                console.error(e);

            });

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            container.appendChild(renderer.domElement);

            window.addEventListener('resize', onWindowResize);

            // stats
            stats = new Stats();
            container.appendChild(stats.dom);

        }

        function createGUI(model, animations) {

            const states = ['Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing'];
            const emotes = ['Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp'];

            gui = new GUI();

            mixer = new THREE.AnimationMixer(model);

            actions = {};

            for (let i = 0; i < animations.length; i++) {

                const clip = animations[i];
                const action = mixer.clipAction(clip);
                actions[clip.name] = action;

                if (emotes.indexOf(clip.name) >= 0 || states.indexOf(clip.name) >= 4) {

                    action.clampWhenFinished = true;
                    action.loop = THREE.LoopOnce;

                }

            }

            // states

            const statesFolder = gui.addFolder('States');

            const clipCtrl = statesFolder.add(api, 'state').options(states);

            clipCtrl.onChange(function () {

                fadeToAction(api.state, 0.5);

            });

            statesFolder.open();

            // emotes

            const emoteFolder = gui.addFolder('Emotes');

            function createEmoteCallback(name) {

                api[name] = function () {

                    fadeToAction(name, 0.2);

                    mixer.addEventListener('finished', restoreState);

                };

                emoteFolder.add(api, name);

            }

            function restoreState() {

                mixer.removeEventListener('finished', restoreState);

                fadeToAction(api.state, 0.2);

            }

            for (let i = 0; i < emotes.length; i++) {

                createEmoteCallback(emotes[i]);

            }

            emoteFolder.open();

            // expressions

            face = model.getObjectByName('Head_4');//返回名称为Head_4的Object3D对象

            const expressions = Object.keys(face.morphTargetDictionary);
            const expressionFolder = gui.addFolder('Expressions');

            for (let i = 0; i < expressions.length; i++) {

                expressionFolder.add(face.morphTargetInfluences, i, 0, 1, 0.01).name(expressions[i]);

            }

            activeAction = actions['Walking'];
            activeAction.play();

            expressionFolder.open();

        }

        // 
        function fadeToAction(name, duration) {

            previousAction = activeAction;
            activeAction = actions[name];

            if (previousAction !== activeAction) {

                previousAction.fadeOut(duration);

            }

            activeAction
                .reset()
                .setEffectiveTimeScale(1)
                .setEffectiveWeight(1)
                .fadeIn(duration)
                .play();

        }

        function onWindowResize() {

            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();

            renderer.setSize(window.innerWidth, window.innerHeight);

        }

        //

        function animate() {
            
            const dt = clock.getDelta();//启用

            if (mixer) mixer.update(dt);

            requestAnimationFrame(animate);

            renderer.render(scene, camera);

            stats.update();

        }

    </script>

</body>

</html>